/*
 * SkinnedModelProcessor.cs
 * Author: Bruno Evangelista
 * Copyright (c) 2008 Bruno Evangelista. All rights reserved.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 */
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using SgMotion;
using SgMotion.Pipeline;
using AnimationChannel = Microsoft.Xna.Framework.Content.Pipeline.Graphics.AnimationChannel;

namespace SgMotionPipeline.Pipeline
{

    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to apply custom processing to content data, converting an object of
    /// type TInput to TOutput. The input and output types may be the same if
    /// the processor wishes to alter data without changing its type.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    ///
    /// </summary>
    [ContentProcessor(DisplayName = "Model - SgMotion (Forward)")]
    public class SkinnedModelProcessor : ContentProcessor<NodeContent, SkinnedModelContent>
    {

        private int _memorySaved = 0;
        private int _vertexCount = 0;

        //Merge animations extension as documented in Shawn Hargreaves blog http://blogs.msdn.com/b/shawnhar/archive/2010/06/18/merging-animation-files.aspx
        ///<summary>
        /// MErge animations lists a series of filenames that should me merged into a single animation
        ///</summary>
        [DisplayName("Merge Animations"),
        Description("Merge several animation into a single XNB. List the filenames relative to the root of the content project seperated by ;"),
        DefaultValue(typeof(string), "")]
        public string MergeAnimations { get; set; }

        void MergeAnimation(NodeContent input, ContentProcessorContext context, string mergeFile)
        {
            NodeContent mergeModel = context.BuildAndLoadAsset<NodeContent, NodeContent>(
                                                new ExternalReference<NodeContent>(mergeFile), null);

            BoneContent rootBone = MeshHelper.FindSkeleton(input);

            if (rootBone == null)
            {
                context.Logger.LogWarning(null, input.Identity, "Source model has no root bone.");
                return;
            }

            BoneContent mergeRoot = MeshHelper.FindSkeleton(mergeModel);

            if (mergeRoot == null)
            {
                context.Logger.LogWarning(null, input.Identity, "Merge model '{0}' has no root bone.", mergeFile);
                return;
            }

            foreach (string animationName in mergeRoot.Animations.Keys)
            {
                string destinationAnimationName = GetDestinationAnimationName(mergeModel.Identity.SourceFilename, animationName);

                //If this animation already exists theres a problem
                if (rootBone.Animations.ContainsKey(destinationAnimationName))
                {
                    context.Logger.LogWarning(null, input.Identity,
                    "Cannot merge animation '{0}' from '{1}', because this animation already exists.",
                    destinationAnimationName, mergeFile);

                    continue;
                }

                context.Logger.LogImportantMessage("Merging animation '{0}' from '{1} (originally {2})'.", destinationAnimationName, Path.GetFileName(mergeModel.Identity.SourceFilename), animationName);

                //Rename the animation
                mergeRoot.Animations[animationName].Name = destinationAnimationName;
                rootBone.Animations.Add(destinationAnimationName, mergeRoot.Animations[animationName]);
            }
        }

        /// <summary>
        /// Animation name is the final part of the filename after the _  
        /// e.g. L_KNIGHT_RUN.FBX = RUN
        /// If there is none then use the default
        /// </summary>
        /// <param name="filename">Filename of the model</param>
        /// <param name="defaultAnimationName">name to use for the animation if we cant find a _</param>
        /// <returns></returns>
        private string GetDestinationAnimationName(string filename, string defaultAnimationName)
        {
            string destinationAnimationName = defaultAnimationName;
            if (RenameAnimationFromFileName)
            {
                filename = Path.GetFileNameWithoutExtension(filename);
                if (filename != null)
                {
                    var parts = filename.Split('_');
                    if (parts.Length > 1)
                    {
                        //take the last part
                        destinationAnimationName = parts[parts.Length - 1].ToUpperInvariant();
                    }
                }
            }
            return destinationAnimationName;
        }


        private struct SplitTask
        {
            public readonly string Name;
            public readonly float StartTimeSeconds;
            public readonly float EndTimeSeconds;

            public SplitTask(string name, float startTime, float endTime)
            {
                Name = name;
                StartTimeSeconds = startTime;
                EndTimeSeconds = endTime;
            }

            public override string ToString()
            {
                return string.Format("{0} [{1} : {2}]", Name, StartTimeSeconds, EndTimeSeconds);
            }
        }

        /// <summary>
        /// DefaultAnimationFrameRate is the framerate that will be used for animations tht do not specify a framerate
        /// </summary>
        public static readonly int DefaultAnimationFramerate = 60;

        /// <summary>
        /// The number of bones supported by the skinned shader
        /// </summary>
        public static int MaxSupportedBones = 75;

        /// <summary>
        /// Get the name of the processor that should handle the base mesh
        /// </summary>
        /// <returns></returns>
        protected virtual string GetSunBurnProcessorName()
        {
            return "LightingSystemModelProcessor";
        }

        #region Content Properties

        private bool _bakeMeshTransforms = true;

        ///<summary>
        /// Bakes the mesh transformations into the geometry
        ///</summary>
        [DisplayName("Bake Mesh Transforms"), Description("If enabled, bake the mesh transform into its geometry."), DefaultValue(typeof(bool), "true")]
        public virtual bool BakeMeshTransforms
        {
            get { return _bakeMeshTransforms; }
            set { _bakeMeshTransforms = value; }
        }

        ///<summary>
        /// Renames animations based on the final part of the filename
        ///</summary>
        [DisplayName("Rename Animation from filename"), Description("If enabled the animation will be named based on the final part of the filename. Anything after the last _"), DefaultValue(typeof(bool), "false")]
        public virtual bool RenameAnimationFromFileName { get; set; }


        ///<summary>
        /// Generates tangent frame and binormal for each vertex
        ///</summary>
        [DisplayName("Generate Tangent Frame"), Description("If enabled, generates the tangent and binormal for each mesh's vertex."), DefaultValue(typeof(bool), "false")]
        public virtual bool GenerateTangentFrame { get; set; }

        [DisplayName("Texture Format"), Description("Specifies the SurfaceFormat type of processed textures. Textures can either remain unchanged from the source asset, converted to the Color format, or DXT compressed."), DefaultValue(TextureFormat.NoChange)]
        public virtual TextureFormat TextureFormat { get; set; }

        ///<summary>
        /// Describes the file that will be used to split animations
        ///</summary>
        [DisplayName("Split Animation Filename"), Description("If set, this file will be used to split the model's animations."), DefaultValue(typeof(string), "")]
        public virtual string SplitAnimationFilename { get; set; }

        ///<summary>
        /// Should we try to remove unwanted UV channels
        ///</summary>
        [DisplayName("Tidy UV channels"), Description("If set, all UV channels except UV0 will be removed."), DefaultValue(typeof(bool), "false")]
        public virtual bool TidyUVChannels { get; set; }

        ///<summary>
        /// Should we try to remove unwanted Color Channels
        ///</summary>
        [DisplayName("Remove Color Channel"), Description("If set, the color channel will be removed from the model."), DefaultValue(typeof(bool), "false")]
        public virtual bool RemoveColorChannels { get; set; }

        ///<summary>
        /// Should we try to remove unwanted Color Channels
        ///</summary>
        [DisplayName("Compress Vertex Buffer"), Description("If set, the vertex buffer will be converted to packed format"), DefaultValue(typeof(bool), "false")]
        public virtual bool CompressVertexBuffer { get; set; }


        /*
         
        ///<summary>
        /// The path to look for the textures
        ///</summary>
        [DisplayName("Texture Path"), Description("If set, changes the path of all texture resources. " +
                                                  "The new path can be an absolute or relative path."), DefaultValue(typeof(string), "")]
        public virtual string TexturePath { get; set; }

        ///<summary>
        /// Is the texture path absolute or relative
        ///</summary>
        [DisplayName("Texture Path Type"), Description("Indicates whether the \"Texture Path\" is an absolute or relative path." +
                                                       "If the path is relative, it is relative to the source asset path."), DefaultValue(typeof(PathType), "Relative")]
        public virtual PathType TexturePathType { get; set; }
        ///<summary>
        ///Should the textures be looked for in TexturePath or use the ones in the file 
        ///</summary>
        [DisplayName("Change Texture Path Enabled"), Description("Indicates whether the \"Change Texture Path\" parameter is enabled."), DefaultValue(typeof(bool), "false")]
        public virtual bool ChangeTexturePathEnabled { get; set; }
         
         */


        ///<summary>
        /// Rotate the model as its imported
        ///</summary>
        [DisplayName("Model Rotation"), Description("Rotates the model a specified number of degrees over the Y, X and Z axis in this order."), DefaultValue(typeof(Vector3), "0, 0, 0")]
        public virtual Vector3 ModelRotate { get; set; }


        private float _modelScale = 1f;

        /// <summary>
        /// Scales the model as it is loaded
        /// </summary>
        [DisplayName("Model Scaling"), Description("Uniformly scales the model."), DefaultValue(typeof(float), "1")]
        public virtual float ModelScale
        {
            get { return _modelScale; }
            set { _modelScale = value; }
        }

        /// <summary>
        /// Skips frames to change the framerate
        /// </summary>
        [DisplayName("Frames to Skip"), Description("Skips frames to change the frame rate"), DefaultValue(typeof(int), "0")]
        public virtual int FramesToSkip { get; set; }

        /// <summary>
        /// Dumps the model during building for debugging
        /// </summary>
        [DisplayName(".Dump Hierarchy"), Description("Dump model hierarchy for debugging."), DefaultValue(typeof(bool), "false")]
        public virtual bool DumpHierarchyEnabled { get; set; }

        /// <summary>
        /// Dumps the animation during building for debugging
        /// </summary>
        [DisplayName(".Dump Animations"), Description("Dump model animations for debugging."), DefaultValue(typeof(bool), "false")]
        public virtual bool DumpAnimationsEnabled { get; set; }

        ///<summary>
        /// Swaps the winding order of the model at build time
        ///</summary>
        [DisplayName("Swap Winding Order"), Description("Used if your models seems drawn inside-out"), DefaultValue(typeof(bool), "false")]
        public virtual bool SwapWindingOrder { get; set; }

        #endregion

        /// <summary>
        /// Process the data
        /// </summary>
        /// <param name="input"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override SkinnedModelContent Process(NodeContent input, ContentProcessorContext context)
        {
            if (input == null)
                throw new ArgumentNullException("input");


            //Rename the main animation to match the filename
            BoneContent mergeRoot = MeshHelper.FindSkeleton(input);
            foreach (var animation in mergeRoot.Animations.Values)
            {
                string destinationAnimationName = GetDestinationAnimationName(input.Identity.SourceFilename, animation.Name);
                context.Logger.LogImportantMessage("Renaming animation '{0}' from '{1} (originally {2})'.", destinationAnimationName, Path.GetFileName(input.Identity.SourceFilename), animation.Name);
                animation.Name = destinationAnimationName;
                break; //we know there is only one to process
            }

            //Merge animations extension as documented in Shawn Hargreaves blog http://blogs.msdn.com/b/shawnhar/archive/2010/06/18/merging-animation-files.aspx
            if (!string.IsNullOrEmpty(MergeAnimations))
            {
                foreach (string mergeFile in MergeAnimations.Split(';')
                                                            .Select(s => s.Trim())
                                                            .Where(s => !string.IsNullOrEmpty(s)))
                {
                    MergeAnimation(input, context, mergeFile);
                }
            }


            if (DumpHierarchyEnabled)
                DumpHierarchy(input, context);
            if (DumpAnimationsEnabled)
                DumpAnimations(input, context);

            BoneContent rootBone = MeshHelper.FindSkeleton(input);
            ValidateModel(rootBone, context);

            // Transform scene according to user defined rotation and scale
            TransformScene(input);

            // Processes model's meshes
            ProcessMeshes(input, context);

            // Process model's materials
            //ProcessMaterials(input, context);

            // Processes model's skeleton
            var skinnedModelBoneCollection = ProcessBones(rootBone, context);

            // Processes model's animations
            var animationClipDictionary = ProcessAnimations(input, rootBone.Animations, skinnedModelBoneCollection, context);

            // Parameters - Opened up properties to the properties window.
            var parameters = new OpaqueDataDictionary
                                 {
                                     {"SwapWindingOrder", SwapWindingOrder },
                                     {"TextureFormat", Enum.GetName(typeof(TextureFormat), TextureFormat)}
                                     //The sunburn processor doesn't have this option which means the base processor does the default and calls it
                                     //Whih conflicts with out vertex buffer compression. However if you get sgMotion to make the tangent information then its all OK
                                     //{"GenerateTangentFrames", false}, //We've already done this so no need to have the model processor do it. In fact it wont work since we may have packed the vertex buffer
                                 };


            // Uses the default model processor
            ModelContent modelContent = context.Convert<NodeContent, ModelContent>(input, GetSunBurnProcessorName(), parameters);

            if (CompressVertexBuffer)
            {
                context.Logger.LogImportantMessage("#### Total Memory saved={0}, Vertices = {1}", _memorySaved, _vertexCount);
            }

            // Return a new skinned model
            return new SkinnedModelContent(modelContent, skinnedModelBoneCollection, animationClipDictionary);
        }

        
        private void TransformScene(NodeContent input)
        {
            Matrix transform = Matrix.Identity;

            // Rotate transfom
            if (ModelRotate != Vector3.Zero)
            {
                Vector3 degreeRotation;
                degreeRotation.X = MathHelper.ToRadians(ModelRotate.X);
                degreeRotation.Y = MathHelper.ToRadians(ModelRotate.Y);
                degreeRotation.Z = MathHelper.ToRadians(ModelRotate.Z);

                transform = Matrix.CreateRotationY(degreeRotation.Y) *
                    Matrix.CreateRotationX(degreeRotation.X) *
                        Matrix.CreateRotationZ(degreeRotation.Z);
            }

            // Scale transform
            if (ModelScale != 1)
            {
                transform = Matrix.CreateScale(ModelScale) * transform;
            }

            // Transform scene
            if (transform != Matrix.Identity)
            {
                MeshHelper.TransformScene(input, transform);
            }
        }

        private void ProcessMeshes(NodeContent node, ContentProcessorContext context)
        {
            var mesh = node as MeshContent;
            if (mesh != null)
            {
                // Validate mesh
                if (!ValidateMesh(mesh, context))
                    return;

                // Process the entire mesh
                ProcessMesh(mesh, context);

                // Now process each of its geometries
                foreach (GeometryContent geometry in mesh.Geometry)
                {
                    ProcessGeometry(geometry, context);
                }
            }

            foreach (NodeContent child in node.Children)
                ProcessMeshes(child, context);
        }

        /// <summary>
        /// Process the mesh
        /// </summary>
        /// <param name="mesh"></param>
        /// <param name="context"></param>
        protected virtual void ProcessMesh(MeshContent mesh, ContentProcessorContext context)
        {
            if (BakeMeshTransforms)
            {
                Matrix vertexTransform = mesh.AbsoluteTransform;

                // Transform the position of all the vertices
                for (int i = 0; i < mesh.Positions.Count; i++)
                {
                    mesh.Positions[i] = Vector3.Transform(mesh.Positions[i], vertexTransform);
                }
            }

            if (GenerateTangentFrame)
            {
                MeshHelper.CalculateTangentFrames(mesh,
                    VertexChannelNames.TextureCoordinate(0),
                    VertexChannelNames.Tangent(0),
                    VertexChannelNames.Binormal(0));
            }

            //Compressing to packed format must be done after the tangent frame information is calculated in the model processor - the function won't work with packed
            if (CompressVertexBuffer)
            {
                CompressVB(mesh, context);
            }

            // Reorder vertex and index data so triangles will render in
            // an order that makes efficient use of the GPU vertex cache.
            MeshHelper.OptimizeForCache(mesh);
        }


        /// <summary>
        /// Use packed vector formts wherever possible
        /// </summary>
        /// <param name="mesh">mesh to compress</param>
        /// <param name="context">content context for output information</param>
        private void CompressVB(MeshContent mesh, ContentProcessorContext context)
        {
            foreach (GeometryContent geometry in mesh.Geometry)
            {
                _vertexCount += geometry.Vertices.VertexCount;

                //TODO - work out how and if we should compress the Position data
                //memory = (Marshal.SizeOf(channel.ElementType) - Marshal.SizeOf(typeof(HalfVector4))) * channel.Count;
                //_memorySaved += memory;
                //geometry.Vertices.Positions.ConvertChannelContent<HalfVector4>(vertexChannelIndex);
                //context.Logger.LogImportantMessage("      Converted {0} to HalfVector4 saving {1} bytes", channel.Name, memory);
                //break;


                for (int i = geometry.Vertices.Channels.Count - 1; i >= 0; i--)
                {
                    int memory;
                    var channel = geometry.Vertices.Channels[i];

                    context.Logger.LogImportantMessage("Compressing Channels {0}:{1}:{2} ({3} vertices)", geometry.Parent.Name, geometry.Name, channel.Name, channel.Count);

                    //How to switch formats - see http://forums.create.msdn.com/forums/p/39154/228971.aspx#228971
                    //and http://forums.create.msdn.com/forums/p/27254/149891.aspx#149891
                    //see http://blogs.msdn.com/b/shawnhar/archive/2008/04/11/santa-s-production-line.aspx for suggested formats
                    //see http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.packedvector.aspx for available packed formats
                    switch (channel.Name)
                    {
                        case "TextureCoordinate0":
                            memory = (Marshal.SizeOf(channel.ElementType) - Marshal.SizeOf(typeof (HalfVector2)))*channel.Count;
                            _memorySaved += memory;
                            geometry.Vertices.Channels.ConvertChannelContent<HalfVector2>(i);
                            context.Logger.LogImportantMessage("      Converted {0} to HalfVector2 saving {1} bytes", channel.Name, memory);
                            break;

                        case "Normal0":
                        case "Binormal0":
                        case "Tangent0":
                            memory = (Marshal.SizeOf(channel.ElementType) - Marshal.SizeOf(typeof (NormalizedShort4)))*channel.Count;
                            _memorySaved += memory;
                            geometry.Vertices.Channels.ConvertChannelContent<NormalizedShort4>(i);
                            context.Logger.LogImportantMessage("      Converted {0} to NormalizedShort4 saving {1} bytes", channel.Name, memory);
                            break;
                            
                        case "BlendWeight0":
                            memory = (Marshal.SizeOf(channel.ElementType) - Marshal.SizeOf(typeof (HalfVector4)))*channel.Count;
                            _memorySaved += memory;
                            geometry.Vertices.Channels.ConvertChannelContent<HalfVector4>(i);
                            context.Logger.LogImportantMessage("      Converted {0} to HalfVector4 saving {1} bytes", channel.Name, memory);
                            break;

                         //BlendIndices is already a Byte4 - there's nothing more packed without playing silly shader games.
                    }
                }
            }
        }

        /// <summary>
        /// Process the geometry
        /// </summary>
        /// <param name="geometry"></param>
        /// <param name="context"></param>
        protected virtual void ProcessGeometry(GeometryContent geometry, ContentProcessorContext context)
        {

            for (int i = geometry.Vertices.Channels.Count - 1; i >= 0; i--)
            {
                var channel = geometry.Vertices.Channels[i];
                context.Logger.LogImportantMessage(channel.Name);

                if (TidyUVChannels)
                {
                    if (channel.Name.StartsWith("TextureCoordinate") && channel.Name != "TextureCoordinate0")
                    {
                        context.Logger.LogImportantMessage("Removed Vertex Channel {0} - unused UV", channel.Name);
                        geometry.Vertices.Channels.RemoveAt(i);
                    }
                }

                if (RemoveColorChannels)
                {
                    if (channel.Name.StartsWith("Color"))
                    {
                        context.Logger.LogImportantMessage("Removed Vertex Channel {0} - Color", channel.Name);
                        geometry.Vertices.Channels.RemoveAt(i);
                    }
                }
            }


            if (BakeMeshTransforms)
            {
                Matrix vertexTransform = geometry.Parent.AbsoluteTransform;
                Matrix vectorTransform = Matrix.Transpose(Matrix.Invert(vertexTransform));

                foreach (VertexChannel vertexChannel in geometry.Vertices.Channels)
                {
                    if (IsVectorChannel(vertexChannel) && vertexChannel.ElementType == typeof(Vector3))
                    {
                        // Cast for a channel of type Vector3
                        var vectorChannel = (VertexChannel<Vector3>)vertexChannel;

                        // Transform all the vectors
                        for (int i = 0; i < vectorChannel.Count; i++)
                        {
                            vectorChannel[i] = Vector3.Transform(vectorChannel[i], vectorTransform);
                            vectorChannel[i].Normalize();
                        }
                    }
                }
            }
        }


        /// <summary>
        /// Traverses the model's mesh parts and processes each of its materials.
        /// Although the materials can be shared between multiple mesh parts, each material is 
        /// processed one unique time.
        /// </summary>
        /// <param name="context">
        ///   The content processor context.
        ///   The content processor context.
        ///   The content processor context.
        /// </param>
        /// <summary>
        /// Traverses the model's mesh parts and processes each of its materials.
        /// Although the materials can be shared between multiple mesh parts, each material is 
        /// processed one unique time.
        /// </summary>
        /// <summary>
        /// Processes the material of each MeshPart in a model.
        /// </summary>
        /// <returns>The processed material.</returns>
        /// <summary>
        /// Extract and processes all the bones (BoneContent) of the model generating a 
        /// SkinnedModelBoneCollection.
        /// </summary>
        /// <param name="rootBone"></param>
        /// <returns></returns>
        private static SkinnedModelBoneContentCollection ProcessBones(BoneContent rootBone,
            ContentProcessorContext context)
        {
            var skinnedBoneList = new List<SkinnedModelBoneContent>(MaxSupportedBones);

            ProcessBones(rootBone, null, skinnedBoneList, context);
            return new SkinnedModelBoneContentCollection(skinnedBoneList);
        }

        /// <summary>
        /// Recursively process each BoneContent of the model generating a new SkinnedModelBone
        /// </summary>
        /// <param name="boneContent"></param>
        /// <param name="skinnedBoneList"></param>
        /// <param name="skinnedModelParentBone"></param>
        /// <param name="context">Processor context</param>
        /// <returns></returns>
        private static SkinnedModelBoneContent ProcessBones(BoneContent boneContent,
            SkinnedModelBoneContent skinnedModelParentBone,
            List<SkinnedModelBoneContent> skinnedBoneList, ContentProcessorContext context)
        {
            // Add the current boneContent to the skinned boneContent list
            var boneIndex = (ushort)skinnedBoneList.Count;

            // Decompose boneContent bind pose from the transform matrix
            Pose bindPose;
            boneContent.Transform.Decompose(out bindPose.Scale, out bindPose.Orientation,
                out bindPose.Translation);

            // Calculates boneContent inverse bind pose
            Matrix inverseBindPose = Matrix.Invert(boneContent.AbsoluteTransform);

            // Create the skinned model's boneContent and add it to the skinned model's boneContent list
            var skinnedModelBone = new SkinnedModelBoneContent(boneIndex, boneContent.Name, bindPose, inverseBindPose);
            skinnedBoneList.Add(skinnedModelBone);

            // Process all children
            var skinnedBoneChildrenList = new List<SkinnedModelBoneContent>();
            foreach (NodeContent nodeContent in boneContent.Children)
            {
                // Validate the bone
                if (!ValidateBone(nodeContent, context))
                    continue;

                var childBoneContent = nodeContent as BoneContent;
                var skinnedBoneChild = ProcessBones(childBoneContent, skinnedModelBone, skinnedBoneList, context);

                skinnedBoneChildrenList.Add(skinnedBoneChild);
            }

            // Sets boneContent parent and children
            skinnedModelBone.Parent = skinnedModelParentBone;
            skinnedModelBone.Children = new SkinnedModelBoneContentCollection(skinnedBoneChildrenList);

            return skinnedModelBone;
        }

        private AnimationClipContentDictionary ProcessAnimations(NodeContent input,
            AnimationContentDictionary animationDictionary,
            IEnumerable<SkinnedModelBoneContent> boneCollection,
            ContentProcessorContext context)
        {
            // Create a collection here (Does not need a dictionary here)
            var animationClipDictionary = new Dictionary<string, AnimationClipContent>();

            foreach (AnimationContent animation in animationDictionary.Values)
            {
                // Validate animation
                if (!ValidateAnimation(animation, context))
                    continue;

                var animationChannelDictionary = new Dictionary<string, AnimationChannelContent>();

                // Process each animation channel (One channel per bone)
                foreach (KeyValuePair<string, AnimationChannel> animationChannelPair in
                    animation.Channels)
                {
                    // Validate animation channel
                    if (!ValidateAnimationChannel(animationChannelPair, animation, boneCollection, context))
                    {
                        continue;
                    }

                    var keyframeList = new List<AnimationKeyframeContent>(animationChannelPair.Value.Count);

                    // Process all the keyframes of that channel
                    foreach (AnimationKeyframe channelKeyframe in animationChannelPair.Value)
                    {
                        // Extract the keyframe pose from its transform matrix
                        Pose keyframePose;
                        channelKeyframe.Transform.Decompose(out keyframePose.Scale, out keyframePose.Orientation, out keyframePose.Translation);

                        keyframeList.Add(new AnimationKeyframeContent(channelKeyframe.Time, keyframePose));
                    }

                    // Sort the keyframes by time
                    keyframeList.Sort();

                    //Remove some of the frames if required
                    if (FramesToSkip > 0)
                    {
                        int removalCount = 0;
                        string message = String.Format("{0}:{2} HAD {1} frames", animation.Name, keyframeList.Count, animationChannelPair.Key);

                        for (int i = 1; i < keyframeList.Count - 1; ) //start at 1 so we keep the 1st frame, -1 becuase we dont want to remove the last frame either
                        {
                            if (++removalCount > FramesToSkip)
                            {
                                i++;
                                removalCount = 0;
                            }
                            else
                            {
                                keyframeList.RemoveAt(i); //No need to increment loop counter - the whole list just moved down
                            }
                        }
                        context.Logger.LogImportantMessage("{0} now has {1} frames", message, keyframeList.Count);
                    }

                    animationChannelDictionary.Add(animationChannelPair.Key, new AnimationChannelContent(keyframeList));
                }

                var animationClip = new AnimationClipContent(
                    animation.Name, new AnimationChannelContentDictionary(animationChannelDictionary),
                    animation.Duration);

                animationClipDictionary.Add(animation.Name, animationClip);
            }

            // Split animations
            if (!string.IsNullOrEmpty(SplitAnimationFilename))
            {
                SplitAnimations(input, animationClipDictionary, context);
            }

            return new AnimationClipContentDictionary(animationClipDictionary);
        }

        private static string ParseChildElementInnerText(XmlElement element, string childName)
        {
            foreach (XmlNode childNode in element)
            {
                var childElement = childNode as XmlElement;
                if (childElement != null && childElement.Name.ToLower() == childName)
                {
                    return childElement.InnerText;
                }
            }

            return null;
        }

        private static List<SplitTask> ParseSplitAnimationTaskList(XmlElement animationElement,
            int animationFramerate, TimeSpan animationDuration, ContentProcessorContext context)
        {
            var splitAnimations = new List<SplitTask>();
            var splitAnimationNames = new List<string>();

            string animationName = ParseChildElementInnerText(animationElement, "name");
            foreach (XmlNode childNode in animationElement.ChildNodes)
            {
                var childElement = childNode as XmlElement;
                if (childElement != null && childElement.Name.ToLower() == "splittask")
                {
                    // Try to get the inner text of all available tags
                    string splitName = ParseChildElementInnerText(childElement, "name");
                    string startTimeText = ParseChildElementInnerText(
                        childElement, "starttimeseconds");
                    string endTimeText = ParseChildElementInnerText(
                        childElement, "endtimeseconds");
                    string startFrameText = ParseChildElementInnerText(
                        childElement, "startframe");
                    string endFrameText = ParseChildElementInnerText(
                        childElement, "endframe");

                    // Check if the split animation has a name
                    if (splitName == null)
                    {
                        throw new InvalidContentException(string.Format(
                            "An split animation on animation {0} does not have tag.",
                                animationName));
                    }

                    // Check if the user have defined both start time and frame
                    if (startTimeText != null && startFrameText != null)
                    {
                        context.Logger.LogWarning(null, null, string.Format(
                            "Split animation {0} on animation {1} has both start time and " +
                            "start frame tags. Start frame will be discarded.",
                            splitName, animationName));
                    }

                    // Check if the user have defined both end time and frame
                    if (endTimeText != null && endFrameText != null)
                    {
                        context.Logger.LogWarning(null, null, string.Format(
                            "Split animation {0} on animation {1} has both end time and " +
                            "end frame tags. End frame will be discarded.",
                            splitName, animationName));
                    }

                    // Parse start time tag
                    float startTime;
                    if (startTimeText != null)
                    {
                        try
                        {
                            startTime = float.Parse(startTimeText);
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Split animation {0} on animation {1} has an invalid start time.",
                                splitName, animationName));
                        }
                    }
                    else if (startFrameText != null)
                    {
                        try
                        {
                            int startFrame = int.Parse(startFrameText);
                            startTime = startFrame / (float)animationFramerate;
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Split animation {0} on animation {1} has an invalid start time.",
                                splitName, animationName));
                        }
                    }
                    else
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} does not have a start time tag.",
                            splitName, animationName));
                    }

                    // Parse end time tag
                    float endTime;
                    if (endTimeText != null)
                    {
                        try
                        {
                            endTime = float.Parse(endTimeText);
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Split animation {0} on animation {1} has an invalid end time.",
                                splitName, animationName));
                        }
                    }
                    else if (endFrameText != null)
                    {
                        try
                        {
                            int endFrame = int.Parse(endFrameText);
                            endTime = endFrame / (float)animationFramerate;
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Split animation {0} on animation {1} has an invalid end time.",
                                splitName, animationName));
                        }
                    }
                    else
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} does not have an end time tag.",
                            splitName, animationName));
                    }

                    // Validate split animation name
                    if (splitAnimationNames.Contains(splitName))
                    {
                        throw new InvalidContentException(string.Format(
                            "Animation {0} contains two split animations named {1}.",
                            animationName, splitName));
                    }
                    splitAnimationNames.Add(splitName);

                    // Validate split animation duration
                    if (endTime <= startTime)
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} has a duration less than or " +
                            "equals to zero.", splitName, animationName));
                    }

                    // Validate split animation start time
                    if (startTime < 0)
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} does not have a positive " +
                            "start time.", splitName, animationName));
                    }

                    // Validate split animation end time
                    if (endTime > animationDuration.TotalSeconds)
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} has an end time bigger than " +
                            "the original animation duration.", splitName, animationName));
                    }

                    splitAnimations.Add(new SplitTask(splitName, startTime, endTime));
                }
            }

            return splitAnimations;
        }

        private void SplitAnimations(NodeContent input, IDictionary<string, AnimationClipContent> animationDictionary, ContentProcessorContext context)
        {
            string sourceAssetPath = Path.GetDirectoryName(input.Identity.SourceFilename);
            // ReSharper disable AssignNullToNotNullAttribute 
            string fullFilePath = Path.GetFullPath(Path.Combine(sourceAssetPath, SplitAnimationFilename));
            // ReSharper restore AssignNullToNotNullAttribute


            //Add a dependency on the XML file so that modifications to it will cause a rebuild
            context.AddDependency(fullFilePath);

            // Read the XML document
            XmlDocument xmlDocument;
            try
            {
                if (!File.Exists(fullFilePath))
                    throw new FileNotFoundException();

                xmlDocument = new XmlDocument();
                xmlDocument.Load(fullFilePath);
            }
            catch (FileNotFoundException)
            {
                throw new InvalidContentException("Missing animation split file: " +
                    fullFilePath);
            }
            catch (XmlException e)
            {
                throw new InvalidContentException(
                    String.Format("Error parsing animation split file: {0}{1}{1}{2}",
                    fullFilePath, Environment.NewLine, e));
            }

            // Find the ANIMATIONS root tag
            XmlElement animationsElement = null;
            foreach (XmlNode xmlNode in xmlDocument.ChildNodes)
            {
                var xmlElement = xmlNode as XmlElement;
                if (xmlElement != null && xmlElement.Name.ToLower() == "animations")
                {
                    animationsElement = xmlElement;
                    break;
                }
            }

            if (animationsElement == null)
            {
                context.Logger.LogWarning(null, null, "Split animation document does not " +
                "contain an <ANIMATIONS> tag and will be skipped.");
                return;
            }

            // Parse each ANIMATION tag
            bool containsAnimationElement = false;
            foreach (XmlNode animationNode in animationsElement.ChildNodes)
            {
                var animationElement = animationNode as XmlElement;
                if (animationElement != null && animationElement.Name.ToLower() == "animation")
                {
                    containsAnimationElement = true;

                    string animationName = ParseChildElementInnerText(
                        animationElement, "name");
                    string animationFramerateText = ParseChildElementInnerText(
                        animationElement, "framerate");

                    if (animationName == null)
                    {
                        throw new InvalidContentException("Split animation document contains " +
                            "an animation that does not have a <NAME> tag.");
                    }

                    int animationFramerate = DefaultAnimationFramerate;
                    if (animationFramerateText == null)
                    {
                        context.Logger.LogWarning(null, null, "Using the default {0} " +
                            "frames per second framerate to split animations.", DefaultAnimationFramerate);
                    }
                    else
                    {
                        try
                        {
                            animationFramerate = int.Parse(animationFramerateText);
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Animation {0} has an invalid framerate.",
                                animationName));
                        }
                    }

                    // Try to get the animation in the animation dictionary
                    AnimationClipContent animationClip;
                    if (animationDictionary.TryGetValue(animationName, out animationClip))
                    {
                        // Get the list of split animations
                        List<SplitTask> splitAnimationTaskList =
                            ParseSplitAnimationTaskList(animationElement, animationFramerate,
                            animationClip.Duration, context);

                        // Get the new animation clips
                        IEnumerable<KeyValuePair<string, AnimationClipContent>> keySplitAnimationClips =
                            SplitAnimation(animationClip, splitAnimationTaskList);

                        // Add each animation clip to the dictionary
                        foreach (KeyValuePair<string, AnimationClipContent> keySplitAnimationClip
                            in keySplitAnimationClips)
                        {
                            animationDictionary.Add(keySplitAnimationClip);
                        }

                    }
                    else
                    {
                        throw new InvalidContentException(string.Format(
                                "Input model does not have animation {0}.",
                                animationName));
                    }
                }
            }

            if (!containsAnimationElement)
            {
                context.Logger.LogWarning(null, null, "Split animation document does not contain " +
                    "any <ANIMATION> tag.");
            }
        }

        private static IEnumerable<KeyValuePair<string, AnimationClipContent>> SplitAnimation(
            AnimationClipContent animationClip, List<SplitTask> splitAnimationTaskList)
        {
            var keyAnimationClipList = new List<KeyValuePair<string, AnimationClipContent>>(splitAnimationTaskList.Count);

            foreach (SplitTask splitAnimationTask in splitAnimationTaskList)
            {
                //context.Logger.LogImportantMessage("Split animation " +
                //splitAnimationTask.Name + " [" + splitAnimationTask.StartTimeSeconds +
                //" : " + splitAnimationTask.EndTimeSeconts + "]");

                var splitAnimationChannelsDictionary = new Dictionary<string, AnimationChannelContent>();

                TimeSpan splitAnimationDuration = TimeSpan.Zero;

                foreach (KeyValuePair<string, AnimationChannelContent> animationChannelPair in
                    animationClip.Channels)
                {
                    var splitKeyframes = new List<AnimationKeyframeContent>();

                    TimeSpan? channelStartTime = null;
                    //string times = "";
                    foreach (AnimationKeyframeContent keyframe in animationChannelPair.Value)
                    {
                        if (keyframe.Time.TotalSeconds >= splitAnimationTask.StartTimeSeconds &&
                            keyframe.Time.TotalSeconds <= splitAnimationTask.EndTimeSeconds)
                        {
                            // Get the time of the first keyframe found
                            if (channelStartTime == null)
                                channelStartTime = keyframe.Time;

                            // Add keyframe
                            TimeSpan newKeyframeTime = keyframe.Time - channelStartTime.Value;
                            splitKeyframes.Add(new AnimationKeyframeContent(
                                newKeyframeTime, keyframe.Pose));

                            if (newKeyframeTime > splitAnimationDuration)
                                splitAnimationDuration = newKeyframeTime;

                            //times += string.Format("{0:F4}, ", newKeyframeTime.TotalSeconds);
                        }
                    }

                    //context.Logger.LogImportantMessage("-- {0}. Times: {1}", 
                    //animationChannelPair.Key, times);

                    if (splitKeyframes.Count > 0)
                        splitAnimationChannelsDictionary.Add(animationChannelPair.Key,
                            new AnimationChannelContent(splitKeyframes));
                }

                // Is it better to set the duration as the time of the last frame?
                //TimeSpan splitAnimationDuration = TimeSpan.FromSeconds(
                //splitAnimationTask.EndTimeSeconts - splitAnimationTask.StartTimeSeconds);

                //splitAnimationDuration += TimeSpan.FromSeconds(3.0f/60);

                // Create a new animation clip
                var splitAnimationClip = new AnimationClipContent(
                    splitAnimationTask.Name,
                    new AnimationChannelContentDictionary(splitAnimationChannelsDictionary),
                    splitAnimationDuration);

                // Add the new animation clip to the animation clip list
                keyAnimationClipList.Add(new KeyValuePair<string, AnimationClipContent>(
                    splitAnimationTask.Name, splitAnimationClip));
            }

            return keyAnimationClipList;
        }

        private static bool IsVectorChannel(VertexChannel vertexChannel)
        {
            return
                (vertexChannel.Name.StartsWith("Normal") || vertexChannel.Name.StartsWith("Tangent") ||
                    vertexChannel.Name.StartsWith("Binormal"));
        }

        private static void ValidateModel(BoneContent rootBone, ContentProcessorContext context)
        {
            // Finds the root bone
            if (rootBone == null)
            {
                throw new InvalidContentException("Input model does not contain a skeleton.");
            }

            // Validate maximum supported bones
            IList<BoneContent> boneList = MeshHelper.FlattenSkeleton(rootBone);
            if (boneList.Count > MaxSupportedBones)
            {
                throw new InvalidContentException(string.Format(
                    "Model's skeleton has {0} bones, but the maximum supported is {1}.",
                    boneList.Count, MaxSupportedBones));
            }

            // Find animations
            AnimationContentDictionary animationDictionary = rootBone.Animations;
            if (animationDictionary.Count == 0)
            {
                context.Logger.LogWarning(null, rootBone.Identity,
                    "Input model does not contain any animation.");
            }

            return;
        }

        private static bool ValidateMesh(MeshContent mesh, ContentProcessorContext context)
        {
            if (mesh.Parent is BoneContent)
            {
                context.Logger.LogWarning(null, mesh.Identity,
                    "Mesh {0} is a child of bone {1}. Meshes that are children of bones might " +
                    "not be handled correct.", mesh.Name, mesh.Parent.Name);
            }

            return true;
        }

        private static bool ValidateBone(NodeContent nodeContent, ContentProcessorContext context)
        {
            var boneContent = nodeContent as BoneContent;
            if (boneContent == null)
            {
                context.Logger.LogWarning(null, nodeContent.Identity, string.Format(
                    "Node {0} is invalid inside the model's skeleton and will be skipped.",
                    nodeContent.Name));

                return false;
            }

            return true;
        }

        private static bool ValidateAnimation(AnimationContent animation, ContentProcessorContext context)
        {
            // Check if this animation has any channel
            if (animation.Channels.Count == 0)
            {
                context.Logger.LogWarning(null, animation.Identity, String.Format(
                    "Animation {0} does not contain any channel and will be skipped.",
                    animation.Name));

                return false;
            }

            // Check if this channel has any keyframe
            if (animation.Duration <= TimeSpan.Zero)
            {
                context.Logger.LogWarning(null, animation.Identity, String.Format(
                    "Animation {0} has a zero duration and will be skipped.", animation.Name));

                return false;
            }

            return true;
        }

        private static bool ValidateAnimationChannel(KeyValuePair<string, AnimationChannel> animationChannelPair,
            AnimationContent parentAnimation, IEnumerable<SkinnedModelBoneContent> boneCollection,
            ContentProcessorContext context)
        {
            // Check if this channel has any keyframe
            if (animationChannelPair.Value.Count == 0)
            {
                context.Logger.LogWarning(null, parentAnimation.Identity, String.Format(
                    "Channel {0} in animation {1} does not contain any keyframe and will be skipped.",
                    animationChannelPair.Key, parentAnimation.Name));

                return false;
            }

            // Check if the animation channel exists in the skeleton
            bool boneFound = false;
            foreach (SkinnedModelBoneContent boneContent in boneCollection)
            {
                if (boneContent.Name.Equals(animationChannelPair.Key))
                {
                    boneFound = true;
                    break;
                }
            }

            if (!boneFound)
            {
                context.Logger.LogWarning(null, parentAnimation.Identity, String.Format(
                    "Channel {0} in animation {1} affects a bone that does not exists in the " +
                    "model's skeleton and will be skipped.", animationChannelPair.Key,
                    parentAnimation.Name));

                return false;
            }

            return true;
        }

        private static void DumpHierarchy(NodeContent input, ContentProcessorContext context)
        {
            context.Logger.LogImportantMessage("Dumping Hierarchy:");
            DumpHierarchy(input, " ", context);
            context.Logger.LogImportantMessage("");
        }

        private static void DumpHierarchy(NodeContent input, string pad, ContentProcessorContext context)
        {
            string[] typeName = input.ToString().Split('.');
            context.Logger.LogImportantMessage(pad + input.Name + " : " +
                typeName[typeName.Length - 1]);

            foreach (NodeContent child in input.Children)
                DumpHierarchy(child, "--" + pad, context);
        }

        private static void DumpAnimations(NodeContent input, ContentProcessorContext context)
        {
            context.Logger.LogImportantMessage("Dumping Animations:");

            BoneContent rootBone = MeshHelper.FindSkeleton(input);
            AnimationContentDictionary animationDictionary = rootBone.Animations;

            if (animationDictionary.Count == 0)
                context.Logger.LogImportantMessage("None.");

            foreach (AnimationContent animation in animationDictionary.Values)
            {
                context.Logger.LogImportantMessage(string.Format("Animation {0}: " +
                    "{1} channels, {2:F4} duration", animation.Name, animation.Channels.Count,
                    animation.Duration.TotalSeconds));

                foreach (KeyValuePair<string, AnimationChannel> animationChannel in animation.Channels)
                {
                    AnimationKeyframe lastKeyframe =
                        animationChannel.Value[animationChannel.Value.Count - 1];

                    //context.Logger.LogImportantMessage(string.Format("-- Channel {0}: " +
                    //"{1:F4} duration.", animationChannel.Key,
                    //lastKeyframe.Time.TotalSeconds));

                    var keyframeTimes = new StringBuilder();
                    if (animationChannel.Value.Count > 0)
                    {
                        keyframeTimes.Append(string.Format("{0:F4}",
                            animationChannel.Value[0].Time.TotalSeconds));

                        for (int i = 1; i < animationChannel.Value.Count; i++)
                        {
                            keyframeTimes.Append(", " + string.Format("{0:F4}",
                                animationChannel.Value[i].Time.TotalSeconds));
                        }

                        //context.Logger.LogImportantMessage(string.Format("---- {0} Keyframes: {1} ",
                        //animationChannel.Value.Count, keyframeTimes));

                        context.Logger.LogImportantMessage(string.Format("-- Channel {0}: " +
                            "{2:F4} duration, {1} keyframes: {3}", animationChannel.Key,
                            animationChannel.Value.Count, lastKeyframe.Time.TotalSeconds,
                            keyframeTimes));

                        //context.Logger.LogImportantMessage("---- Keyframes time: " + keyframeTimes);
                    }
                }

                context.Logger.LogImportantMessage("");
            }

            // Final breakline
            if (animationDictionary.Values.Count <= 0)
                context.Logger.LogImportantMessage("");
        }
    }
}